package com.hama.iotrust.common.mybatis;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.ibatis.binding.BindingException;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.exceptions.TooManyResultsException;
import org.apache.ibatis.executor.BatchResult;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.result.DefaultMapResultHandler;
import org.apache.ibatis.executor.result.DefaultResultContext;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.mapping.MappedStatement.Builder;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.scripting.xmltags.ForEachSqlNode;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;

import com.hama.iotrust.common.mybatis.criteria.Criteria;
import com.hama.iotrust.common.mybatis.dialect.Dialect;
import com.hama.iotrust.common.mybatis.result.PaginationList;

/**
 * @author Jinjq
 */
public class DialectalSqlSession implements SqlSession {

    private Configuration configuration;
    private Executor      executor;

    private boolean       dirty;
    private Dialect       dialect;       // ptma@163.com

    @Deprecated
    public DialectalSqlSession(Configuration configuration, Executor executor, boolean autoCommit){
        this(configuration, executor, null);
    }

    public DialectalSqlSession(Configuration configuration, Executor executor, Dialect dialect){
        this.configuration = configuration;
        this.executor = executor;
        this.dirty = false;
        this.dialect = dialect;
    }

    public <T> T selectOne(String statement) {
        return this.<T> selectOne(statement, null);
    }

    public <T> T selectOne(String statement, Object parameter) {
        // Popular vote was to return null on 0 results and throw exception on too many.
        List<T> list = this.<T> selectList(statement, parameter);
        if (list.size() == 1) {
            return list.get(0);
        } else if (list.size() > 1) {
            throw new TooManyResultsException(
                                              "Expected one result (or null) to be returned by selectOne(), but found: "
                                                              + list.size());
        } else {
            return null;
        }
    }

    public <K, V> Map<K, V> selectMap(String statement, String mapKey) {
        return this.<K, V> selectMap(statement, null, mapKey, RowBounds.DEFAULT);
    }

    public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey) {
        return this.<K, V> selectMap(statement, parameter, mapKey, RowBounds.DEFAULT);
    }

    public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
        final List<?> list = selectList(statement, parameter, rowBounds);
        final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<K, V>(
                                                                                                 mapKey,
                                                                                                 configuration.getObjectFactory(),
                                                                                                 configuration.getObjectWrapperFactory());
        final DefaultResultContext context = new DefaultResultContext();
        for (Object o : list) {
            context.nextResultObject(o);
            mapResultHandler.handleResult(context);
        }
        Map<K, V> selectedMap = mapResultHandler.getMappedResults();
        return selectedMap;
    }

    public <E> List<E> selectList(String statement) {
        return this.<E> selectList(statement, null);
    }

    public <E> List<E> selectList(String statement, Object parameter) {
        return this.<E> selectList(statement, parameter, RowBounds.DEFAULT);
    }

    public CacheKey createCacheKey(MappedStatement ms, final Object parameterObject, RowBounds rowBounds,
                                   BoundSql boundSql) {
        CacheKey cacheKey = new CacheKey();
        cacheKey.update(ms.getId());
        cacheKey.update(rowBounds.getOffset());
        cacheKey.update(rowBounds.getLimit());
        cacheKey.update(boundSql.getSql());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                cacheKey.update(parameterObject);
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        cacheKey.update(metaObject.getValue(propertyName));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        cacheKey.update(boundSql.getAdditionalParameter(propertyName));
                    }
                }
            }
        }
        return cacheKey;
    }

    @SuppressWarnings("unchecked")
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
            MappedStatement ms = configuration.getMappedStatement(statement);
            Criteria<?> criteria = null;
            if (parameter instanceof Criteria<?>) { // 只有一个参数的情况
                criteria = (Criteria<?>) parameter;
            } else if (parameter instanceof Collection) { // 多参数的情况，找到第一个 Criteria 的参数
                Iterator<?> iterator = ((Collection<?>) parameter).iterator();
                while (iterator.hasNext()) {
                    Object item = iterator.next();
                    if (item instanceof Criteria<?>) {
                        criteria = (Criteria<?>) item;
                        break;
                    }
                }
            } else if (parameter instanceof Map) { // 多参数的情况，找到第一个 Criteria 的参数
                for (Map.Entry<String, Object> e : ((Map<String, Object>) parameter).entrySet()) {
                    if (e.getValue() instanceof Criteria<?>) {
                        criteria = (Criteria<?>) e.getValue();
                        break;
                    }
                }
            } else if (parameter != null && parameter.getClass().isArray()) {
                Object[] array = (Object[]) parameter;
                for (int i = 0; i < array.length; i++) {
                    if (array[i] != null && (array[i] instanceof Criteria<?>)) {
                        criteria = (Criteria<?>) array[i];
                        break;
                    }
                }
            }
            List<E> result = null;
            if (criteria != null && criteria.isEnablePagination() && dialect != null && dialect.supportsLimitOffset()) {// 启用分页
                BoundSql boundSql = ms.getBoundSql(parameter);
                Cache cache = ms.getCache();
                if (cache != null) {// 启用了缓存
                    if (ms.isUseCache()) {
                        CacheKey key = createCacheKey(ms, criteria, criteria.getRowBounds(), boundSql);
                        if (!dirty) {
                            cache.getReadWriteLock().readLock().lock();
                            try {
                                PaginationList<E> cachedList = (PaginationList<E>) cache.getObject(key);
                                if (cachedList != null) { // 在缓存中找到了
                                    criteria.setTotalRecords(cachedList.getTotalRecords());
                                    return cachedList;
                                }
                            } finally {
                                cache.getReadWriteLock().readLock().unlock();
                            }
                        }
                        PaginationList<E> paginationList = selectPaginationList(ms, criteria, parameter);
                        criteria.setTotalRecords(paginationList.getTotalRecords());
                        cache.putObject(key, paginationList);
                        return paginationList;
                    }
                }
                PaginationList<E> paginationList = selectPaginationList(ms, criteria, parameter);
                criteria.setTotalRecords(paginationList.getTotalRecords());
                result = paginationList;
            } else {
                result = executor.<E> query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
            }
            return result;
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }

    private <E> PaginationList<E> selectPaginationList(MappedStatement ms, Criteria<?> criteria, Object parameter)
                                                                                                                  throws SQLException {
        PaginationList<E> newResult = new PaginationList<E>();
        BoundSql boundSql = ms.getBoundSql(parameter);
        String sql = boundSql.getSql().trim();

        int rowCount = getTotalRows(ms, boundSql);
        criteria.setTotalRecords(rowCount);
        newResult.setTotalRecords(rowCount);// 记录总数

        int offset = (criteria.getCurrentPage() - 1) * criteria.getPageSize();
        int limit = criteria.getPageSize();
        sql = dialect.getLimitString(sql, offset, limit);
        BoundSql newBoundSql = copyFromBoundSql(ms, boundSql, sql);
        MappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
        List<E> result = executor.<E> query(newMs, wrapCollection(parameter), RowBounds.DEFAULT,
                                            Executor.NO_RESULT_HANDLER);

        newResult.addAll(result);

        return newResult;
    }

    @SuppressWarnings("unchecked")
    private int getTotalRows(MappedStatement ms, BoundSql boundSql) throws SQLException {

        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        Object param = boundSql.getParameterObject();
        MetaObject metaObject = configuration.newMetaObject(param);

        int total = 0;
        String sql = boundSql.getSql();
        String countSql = "SELECT count(1) FROM (" + sql + ") AS T_PAGINATION"; // 记录统计 (mysql要求必须添加 最后的as t)
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            conn = configuration.getEnvironment().getDataSource().getConnection();
            ps = conn.prepareStatement(countSql);
            int i = 1;
            for (ParameterMapping pm : boundSql.getParameterMappings()) {
                Object value = null;
                String propertyName = pm.getProperty();
                PropertyTokenizer prop = new PropertyTokenizer(propertyName);
                if (typeHandlerRegistry.hasTypeHandler(param.getClass())) {
                    value = param;
                } else if (boundSql.hasAdditionalParameter(propertyName)) {
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX)
                           && boundSql.hasAdditionalParameter(prop.getName())) {
                    value = boundSql.getAdditionalParameter(prop.getName());
                    if (value != null) {
                        value = configuration.newMetaObject(value).getValue(propertyName.substring(prop.getName().length()));
                    }
                } else {
                    value = metaObject.getValue(propertyName);
                }
                TypeHandler<Object> typeHandler = (TypeHandler<Object>) pm.getTypeHandler();
                typeHandler.setParameter(ps, i++, value, pm.getJdbcType());
            }
            rs = ps.executeQuery();
            if (rs.next()) total = rs.getInt(1);
        } catch (SQLException e) {
            throw e;
        } finally {
            if (rs != null) rs.close();
            if (ps != null) ps.close();
            if (conn != null) conn.close();
        }
        return total;
    }

    private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource,
                                                      ms.getSqlCommandType());

        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        builder.keyProperty(mergeArrayToString(ms.getKeyProperties()));

        // setStatementTimeout()
        builder.timeout(ms.getTimeout());

        // setStatementResultMap()
        builder.parameterMap(ms.getParameterMap());

        // setStatementResultMap()
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());

        // setStatementCache()
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());

        return builder.build();
    }

    private BoundSql copyFromBoundSql(MappedStatement ms, BoundSql boundSql, String sql) {
        BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), sql, boundSql.getParameterMappings(),
                                            boundSql.getParameterObject());
        for (ParameterMapping mapping : boundSql.getParameterMappings()) {
            String prop = mapping.getProperty();
            if (boundSql.hasAdditionalParameter(prop)) {
                newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
            }
        }
        return newBoundSql;
    }

    private static String mergeArrayToString(String[] in) {
        if (in == null || in.length == 0) {
            return null;
        } else {
            StringBuffer answer = new StringBuffer();
            for (int i = 0; i < in.length; i++) {
                answer.append(in[i]).append(",");
            }
            return answer.substring(0, answer.length() - 1);
        }
    }

    public void select(String statement, Object parameter, ResultHandler handler) {
        select(statement, parameter, RowBounds.DEFAULT, handler);
    }

    public void select(String statement, ResultHandler handler) {
        select(statement, null, RowBounds.DEFAULT, handler);
    }

    public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
        try {
            MappedStatement ms = configuration.getMappedStatement(statement);
            executor.query(ms, wrapCollection(parameter), rowBounds, handler);
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }

    public int insert(String statement) {
        return insert(statement, null);
    }

    public int insert(String statement, Object parameter) {
        return update(statement, parameter);
    }

    public int update(String statement) {
        return update(statement, null);
    }

    public int update(String statement, Object parameter) {
        try {
            dirty = true;
            MappedStatement ms = configuration.getMappedStatement(statement);
            return executor.update(ms, wrapCollection(parameter));
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }

    public int delete(String statement) {
        return update(statement, null);
    }

    public int delete(String statement, Object parameter) {
        return update(statement, parameter);
    }

    public void commit() {
        commit(false);
    }

    public void commit(boolean force) {
        try {
            executor.commit(isCommitOrRollbackRequired(force));
            dirty = false;
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }

    public void rollback() {
        rollback(false);
    }

    public void rollback(boolean force) {
        try {
            executor.rollback(isCommitOrRollbackRequired(force));
            dirty = false;
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error rolling back transaction.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }

    public List<BatchResult> flushStatements() {
        try {
            return executor.flushStatements();
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error flushing statements.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }

    public void close() {
        try {
            executor.close(isCommitOrRollbackRequired(false));
            dirty = false;
        } finally {
            ErrorContext.instance().reset();
        }
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public <T> T getMapper(Class<T> type) {
        return configuration.<T> getMapper(type, this);
    }

    public Connection getConnection() {
        try {
            return executor.getTransaction().getConnection();
        } catch (SQLException e) {
            throw ExceptionFactory.wrapException("Error getting a new connection.  Cause: " + e, e);
        }
    }

    public void clearCache() {
        executor.clearLocalCache();
    }

    private boolean isCommitOrRollbackRequired(boolean force) {
        return dirty || force;
    }

    private Object wrapCollection(final Object object) {
        if (object instanceof List) {
            StrictMap<Object> map = new StrictMap<Object>();
            map.put("list", object);
            return map;
        } else if (object != null && object.getClass().isArray()) {
            StrictMap<Object> map = new StrictMap<Object>();
            map.put("array", object);
            return map;
        }
        return object;
    }

    public static class StrictMap<V> extends HashMap<String, V> {

        private static final long serialVersionUID = -5741767162221585340L;

        @Override
        public V get(Object key) {
            if (!super.containsKey(key)) {
                throw new BindingException("Parameter '" + key + "' not found. Available parameters are "
                                           + this.keySet());
            }
            return super.get(key);
        }

    }

    public static class BoundSqlSqlSource implements SqlSource {

        BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql){
            this.boundSql = boundSql;
        }

        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }
}
